{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Collecting torch==1.9\n",
      "  Using cached torch-1.9.0-cp39-none-macosx_11_0_arm64.whl (41.5 MB)\n",
      "Requirement already satisfied: typing-extensions in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (from torch==1.9) (4.9.0)\n",
      "\u001b[33mDEPRECATION: pytorch-lightning 1.7.0 has a non-standard dependency specifier torch>=1.9.*. pip 24.0 will enforce this behaviour change. A possible replacement is to upgrade to a newer version of pytorch-lightning or contact the author to suggest that they release a version with a conforming dependency specifiers. Discussion can be found at https://github.com/pypa/pip/issues/12063\u001b[0m\u001b[33m\n",
      "\u001b[0mInstalling collected packages: torch\n",
      "  Attempting uninstall: torch\n",
      "    Found existing installation: torch 1.12.1\n",
      "    Uninstalling torch-1.12.1:\n",
      "      Successfully uninstalled torch-1.12.1\n",
      "\u001b[31mERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.\n",
      "torchdata 0.4.1 requires torch==1.12.1, but you have torch 1.9.0 which is incompatible.\u001b[0m\u001b[31m\n",
      "\u001b[0mSuccessfully installed torch-1.9.0\n",
      "Requirement already satisfied: torchtext==0.10 in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (0.10.0)\n",
      "Requirement already satisfied: tqdm in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (from torchtext==0.10) (4.66.2)\n",
      "Requirement already satisfied: requests in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (from torchtext==0.10) (2.31.0)\n",
      "Requirement already satisfied: torch in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (from torchtext==0.10) (1.9.0)\n",
      "Requirement already satisfied: numpy in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (from torchtext==0.10) (1.26.4)\n",
      "Requirement already satisfied: charset-normalizer<4,>=2 in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (from requests->torchtext==0.10) (3.3.2)\n",
      "Requirement already satisfied: idna<4,>=2.5 in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (from requests->torchtext==0.10) (3.6)\n",
      "Requirement already satisfied: urllib3<3,>=1.21.1 in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (from requests->torchtext==0.10) (2.2.1)\n",
      "Requirement already satisfied: certifi>=2017.4.17 in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (from requests->torchtext==0.10) (2024.2.2)\n",
      "Requirement already satisfied: typing-extensions in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (from torch->torchtext==0.10) (4.9.0)\n",
      "\u001b[33mDEPRECATION: pytorch-lightning 1.7.0 has a non-standard dependency specifier torch>=1.9.*. pip 24.0 will enforce this behaviour change. A possible replacement is to upgrade to a newer version of pytorch-lightning or contact the author to suggest that they release a version with a conforming dependency specifiers. Discussion can be found at https://github.com/pypa/pip/issues/12063\u001b[0m\u001b[33m\n",
      "\u001b[0mCollecting matplotlib==3.8.3\n",
      "  Using cached matplotlib-3.8.3-cp39-cp39-macosx_11_0_arm64.whl.metadata (5.8 kB)\n",
      "Requirement already satisfied: contourpy>=1.0.1 in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (from matplotlib==3.8.3) (1.2.0)\n",
      "Requirement already satisfied: cycler>=0.10 in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (from matplotlib==3.8.3) (0.12.1)\n",
      "Requirement already satisfied: fonttools>=4.22.0 in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (from matplotlib==3.8.3) (4.49.0)\n",
      "Requirement already satisfied: kiwisolver>=1.3.1 in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (from matplotlib==3.8.3) (1.4.5)\n",
      "Requirement already satisfied: numpy<2,>=1.21 in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (from matplotlib==3.8.3) (1.26.4)\n",
      "Requirement already satisfied: packaging>=20.0 in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (from matplotlib==3.8.3) (23.2)\n",
      "Requirement already satisfied: pillow>=8 in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (from matplotlib==3.8.3) (9.3.0)\n",
      "Requirement already satisfied: pyparsing>=2.3.1 in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (from matplotlib==3.8.3) (3.1.1)\n",
      "Requirement already satisfied: python-dateutil>=2.7 in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (from matplotlib==3.8.3) (2.8.2)\n",
      "Requirement already satisfied: importlib-resources>=3.2.0 in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (from matplotlib==3.8.3) (6.1.1)\n",
      "Requirement already satisfied: zipp>=3.1.0 in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (from importlib-resources>=3.2.0->matplotlib==3.8.3) (3.17.0)\n",
      "Requirement already satisfied: six>=1.5 in /Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages (from python-dateutil>=2.7->matplotlib==3.8.3) (1.16.0)\n",
      "Using cached matplotlib-3.8.3-cp39-cp39-macosx_11_0_arm64.whl (7.5 MB)\n",
      "\u001b[33mDEPRECATION: pytorch-lightning 1.7.0 has a non-standard dependency specifier torch>=1.9.*. pip 24.0 will enforce this behaviour change. A possible replacement is to upgrade to a newer version of pytorch-lightning or contact the author to suggest that they release a version with a conforming dependency specifiers. Discussion can be found at https://github.com/pypa/pip/issues/12063\u001b[0m\u001b[33m\n",
      "\u001b[0mInstalling collected packages: matplotlib\n",
      "  Attempting uninstall: matplotlib\n",
      "    Found existing installation: matplotlib 3.5.2\n",
      "    Uninstalling matplotlib-3.5.2:\n",
      "      Successfully uninstalled matplotlib-3.5.2\n",
      "Successfully installed matplotlib-3.8.3\n"
     ]
    }
   ],
   "source": [
    "# requires python3.9\n",
    "!pip install torch==1.9\n",
    "!pip install torchtext==0.10\n",
    "!pip install matplotlib==3.8.3"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import time\n",
    "import numpy as np\n",
    "from tqdm import tqdm\n",
    "from string import punctuation\n",
    "from collections import Counter\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.optim as optim\n",
    "from torch.utils.data import DataLoader, TensorDataset\n",
    "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
    "\n",
    "torch.use_deterministic_algorithms(True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "import random\n",
    "from torchtext.legacy import datasets\n",
    "from torchtext.legacy import data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "TEXT_FIELD = data.Field(tokenize = data.get_tokenizer(\"basic_english\"), include_lengths = True)\n",
    "LABEL_FIELD = data.LabelField(dtype = torch.float)\n",
    "\n",
    "train_dataset, test_dataset = datasets.IMDB.splits(TEXT_FIELD, LABEL_FIELD)\n",
    "train_dataset, valid_dataset = train_dataset.split(random_state = random.seed(123))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "MAX_VOCABULARY_SIZE = 25000\n",
    "\n",
    "TEXT_FIELD.build_vocab(train_dataset, \n",
    "                 max_size = MAX_VOCABULARY_SIZE)\n",
    "\n",
    "LABEL_FIELD.build_vocab(train_dataset)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "B_SIZE = 64\n",
    "\n",
    "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
    "\n",
    "train_data_iterator, valid_data_iterator, test_data_iterator = data.BucketIterator.splits(\n",
    "    (train_dataset, valid_dataset, test_dataset), \n",
    "    batch_size = B_SIZE,\n",
    "    sort_within_batch = True,\n",
    "    device = device)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "## If you are training using GPUs, we need to use the following function for the pack_padded_sequence method to work \n",
    "## (reference : https://discuss.pytorch.org/t/error-with-lengths-in-pack-padded-sequence/35517/3)\n",
    "if torch.cuda.is_available():\n",
    "    torch.set_default_tensor_type(torch.cuda.FloatTensor)\n",
    "from torch.nn.utils.rnn import pack_padded_sequence, PackedSequence\n",
    "\n",
    "def cuda_pack_padded_sequence(input, lengths, batch_first=False, enforce_sorted=True):\n",
    "    lengths = torch.as_tensor(lengths, dtype=torch.int64)\n",
    "    lengths = lengths.cpu()\n",
    "    if enforce_sorted:\n",
    "        sorted_indices = None\n",
    "    else:\n",
    "        lengths, sorted_indices = torch.sort(lengths, descending=True)\n",
    "        sorted_indices = sorted_indices.to(input.device)\n",
    "    batch_dim = 0 if batch_first else 1\n",
    "    input = input.index_select(batch_dim, sorted_indices)\n",
    "\n",
    "    data, batch_sizes = \\\n",
    "    torch._C._VariableFunctions._pack_padded_sequence(input, lengths, batch_first)\n",
    "    return PackedSequence(data, batch_sizes, sorted_indices)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/Users/Ashish.Jha/anaconda3/envs/python39/lib/python3.9/site-packages/torch/nn/modules/rnn.py:62: UserWarning: dropout option adds dropout after all but last recurrent layer, so non-zero dropout expects num_layers greater than 1, but got dropout=0.5 and num_layers=1\n",
      "  warnings.warn(\"dropout option adds dropout after all but last \"\n"
     ]
    }
   ],
   "source": [
    "class LSTM(nn.Module):\n",
    "    def __init__(self, vocabulary_size, embedding_dimension, hidden_dimension, output_dimension, dropout, pad_index):\n",
    "        super().__init__()\n",
    "        self.embedding_layer = nn.Embedding(vocabulary_size, embedding_dimension, padding_idx = pad_index)\n",
    "        self.lstm_layer = nn.LSTM(embedding_dimension, \n",
    "                           hidden_dimension, \n",
    "                           num_layers=1, \n",
    "                           bidirectional=True, \n",
    "                           dropout=dropout)\n",
    "        self.fc_layer = nn.Linear(hidden_dimension * 2, output_dimension)\n",
    "        self.dropout_layer = nn.Dropout(dropout)\n",
    "        \n",
    "    def forward(self, sequence, sequence_lengths=None):\n",
    "        if sequence_lengths is None:\n",
    "            sequence_lengths = torch.LongTensor([len(sequence)])\n",
    "        \n",
    "        # sequence := (sequence_length, batch_size)\n",
    "        embedded_output = self.dropout_layer(self.embedding_layer(sequence))\n",
    "        \n",
    "        \n",
    "        # embedded_output := (sequence_length, batch_size, embedding_dimension)\n",
    "        if torch.cuda.is_available():\n",
    "            packed_embedded_output = cuda_pack_padded_sequence(embedded_output, sequence_lengths)\n",
    "        else:\n",
    "            packed_embedded_output = nn.utils.rnn.pack_padded_sequence(embedded_output, sequence_lengths)\n",
    "        \n",
    "        packed_output, (hidden_state, cell_state) = self.lstm_layer(packed_embedded_output)\n",
    "        # hidden_state := (num_layers * num_directions, batch_size, hidden_dimension)\n",
    "        # cell_state := (num_layers * num_directions, batch_size, hidden_dimension)\n",
    "        \n",
    "        op, op_lengths = nn.utils.rnn.pad_packed_sequence(packed_output)\n",
    "        # op := (sequence_length, batch_size, hidden_dimension * num_directions)\n",
    "        \n",
    "        hidden_output = torch.cat((hidden_state[-2,:,:], hidden_state[-1,:,:]), dim = 1)        \n",
    "        # hidden_output := (batch_size, hidden_dimension * num_directions)\n",
    "        \n",
    "        return self.fc_layer(hidden_output)\n",
    "\n",
    "    \n",
    "INPUT_DIMENSION = len(TEXT_FIELD.vocab)\n",
    "EMBEDDING_DIMENSION = 100\n",
    "HIDDEN_DIMENSION = 32\n",
    "OUTPUT_DIMENSION = 1\n",
    "DROPOUT = 0.5\n",
    "PAD_INDEX = TEXT_FIELD.vocab.stoi[TEXT_FIELD.pad_token]\n",
    "\n",
    "lstm_model = LSTM(INPUT_DIMENSION, \n",
    "            EMBEDDING_DIMENSION, \n",
    "            HIDDEN_DIMENSION, \n",
    "            OUTPUT_DIMENSION, \n",
    "            DROPOUT, \n",
    "            PAD_INDEX)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "UNK_INDEX = TEXT_FIELD.vocab.stoi[TEXT_FIELD.unk_token]\n",
    "\n",
    "lstm_model.embedding_layer.weight.data[UNK_INDEX] = torch.zeros(EMBEDDING_DIMENSION)\n",
    "lstm_model.embedding_layer.weight.data[PAD_INDEX] = torch.zeros(EMBEDDING_DIMENSION)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "optim = torch.optim.Adam(lstm_model.parameters())\n",
    "loss_func = nn.BCEWithLogitsLoss()\n",
    "\n",
    "lstm_model = lstm_model.to(device)\n",
    "loss_func = loss_func.to(device)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "def accuracy_metric(predictions, ground_truth):\n",
    "    \"\"\"\n",
    "    Returns 0-1 accuracy for the given set of predictions and ground truth\n",
    "    \"\"\"\n",
    "    # round predictions to either 0 or 1\n",
    "    rounded_predictions = torch.round(torch.sigmoid(predictions))\n",
    "    success = (rounded_predictions == ground_truth).float() #convert into float for division \n",
    "    accuracy = success.sum() / len(success)\n",
    "    return accuracy"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "def train(model, data_iterator, optim, loss_func):\n",
    "    loss = 0\n",
    "    accuracy = 0\n",
    "    model.train()\n",
    "    \n",
    "    for curr_batch in data_iterator:\n",
    "        optim.zero_grad()\n",
    "        sequence, sequence_lengths = curr_batch.text\n",
    "        preds = lstm_model(sequence, sequence_lengths).squeeze(1)\n",
    "        \n",
    "        loss_curr = loss_func(preds, curr_batch.label)\n",
    "        accuracy_curr = accuracy_metric(preds, curr_batch.label)\n",
    "        \n",
    "        loss_curr.backward()\n",
    "        optim.step()\n",
    "        \n",
    "        loss += loss_curr.item()\n",
    "        accuracy += accuracy_curr.item()\n",
    "        \n",
    "    return loss/len(data_iterator), accuracy/len(data_iterator)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "def validate(model, data_iterator, loss_func):\n",
    "    loss = 0\n",
    "    accuracy = 0\n",
    "    model.eval()\n",
    "    \n",
    "    with torch.no_grad():\n",
    "        for curr_batch in data_iterator:\n",
    "            sequence, sequence_lengths = curr_batch.text\n",
    "            preds = model(sequence, sequence_lengths).squeeze(1)\n",
    "            \n",
    "            loss_curr = loss_func(preds, curr_batch.label)\n",
    "            accuracy_curr = accuracy_metric(preds, curr_batch.label)\n",
    "\n",
    "            loss += loss_curr.item()\n",
    "            accuracy += accuracy_curr.item()\n",
    "        \n",
    "    return loss/len(data_iterator), accuracy/len(data_iterator)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch number: 1 | time elapsed: 187.57019424438477s\n",
      "training loss: 0.685 | training accuracy: 54.87%\n",
      "validation loss: 0.665 |  validation accuracy: 59.75%\n",
      "\n",
      "epoch number: 2 | time elapsed: 192.59944200515747s\n",
      "training loss: 0.647 | training accuracy: 61.99%\n",
      "validation loss: 0.618 |  validation accuracy: 68.85%\n",
      "\n",
      "epoch number: 3 | time elapsed: 216.21610808372498s\n",
      "training loss: 0.570 | training accuracy: 70.57%\n",
      "validation loss: 0.592 |  validation accuracy: 69.94%\n",
      "\n",
      "epoch number: 4 | time elapsed: 668.1886627674103s\n",
      "training loss: 0.510 | training accuracy: 75.07%\n",
      "validation loss: 0.556 |  validation accuracy: 75.45%\n",
      "\n",
      "epoch number: 5 | time elapsed: 216.72939014434814s\n",
      "training loss: 0.473 | training accuracy: 77.64%\n",
      "validation loss: 0.592 |  validation accuracy: 74.74%\n",
      "\n",
      "epoch number: 6 | time elapsed: 843.4122788906097s\n",
      "training loss: 0.442 | training accuracy: 79.81%\n",
      "validation loss: 0.561 |  validation accuracy: 76.83%\n",
      "\n",
      "epoch number: 7 | time elapsed: 439.7972311973572s\n",
      "training loss: 0.399 | training accuracy: 82.28%\n",
      "validation loss: 0.662 |  validation accuracy: 74.97%\n",
      "\n",
      "epoch number: 8 | time elapsed: 200.3659451007843s\n",
      "training loss: 0.382 | training accuracy: 83.27%\n",
      "validation loss: 0.584 |  validation accuracy: 77.92%\n",
      "\n",
      "epoch number: 9 | time elapsed: 196.59090566635132s\n",
      "training loss: 0.361 | training accuracy: 84.36%\n",
      "validation loss: 0.533 |  validation accuracy: 79.22%\n",
      "\n",
      "epoch number: 10 | time elapsed: 287.94251704216003s\n",
      "training loss: 0.340 | training accuracy: 85.32%\n",
      "validation loss: 0.577 |  validation accuracy: 80.12%\n",
      "\n"
     ]
    }
   ],
   "source": [
    "num_epochs = 10\n",
    "best_validation_loss = float('inf')\n",
    "\n",
    "for ep in range(num_epochs):\n",
    "\n",
    "    time_start = time.time()\n",
    "    \n",
    "    training_loss, train_accuracy = train(lstm_model, train_data_iterator, optim, loss_func)\n",
    "    validation_loss, validation_accuracy = validate(lstm_model, valid_data_iterator, loss_func)\n",
    "    \n",
    "    time_end = time.time()\n",
    "    time_delta = time_end - time_start \n",
    "    \n",
    "    if validation_loss < best_validation_loss:\n",
    "        best_validation_loss = validation_loss\n",
    "        torch.save(lstm_model.state_dict(), 'lstm_model.pt')\n",
    "    \n",
    "    print(f'epoch number: {ep+1} | time elapsed: {time_delta}s')\n",
    "    print(f'training loss: {training_loss:.3f} | training accuracy: {train_accuracy*100:.2f}%')\n",
    "    print(f'validation loss: {validation_loss:.3f} |  validation accuracy: {validation_accuracy*100:.2f}%')\n",
    "    print()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "test loss: 0.557 | test accuracy: 78.59%\n"
     ]
    }
   ],
   "source": [
    "lstm_model.load_state_dict(torch.load('./lstm_model.pt'))\n",
    "\n",
    "test_loss, test_accuracy = validate(lstm_model, test_data_iterator, loss_func)\n",
    "\n",
    "print(f'test loss: {test_loss:.3f} | test accuracy: {test_accuracy*100:.2f}%')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "def sentiment_inference(model, sentence):\n",
    "    model.eval()\n",
    "    \n",
    "    # text transformations\n",
    "    tokenized = data.get_tokenizer(\"basic_english\")(sentence)\n",
    "    tokenized = [TEXT_FIELD.vocab.stoi[t] for t in tokenized]\n",
    "    \n",
    "    # model inference\n",
    "    model_input = torch.LongTensor(tokenized).to(device)\n",
    "    model_input = model_input.unsqueeze(1)\n",
    "    \n",
    "    pred = torch.sigmoid(model(model_input))\n",
    "    \n",
    "    return pred.item()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.015025223605334759\n",
      "0.006463728845119476\n",
      "0.49227291345596313\n",
      "0.7560229897499084\n"
     ]
    }
   ],
   "source": [
    "print(sentiment_inference(lstm_model, \"This film is horrible\"))\n",
    "print(sentiment_inference(lstm_model, \"Director tried too hard but this film is bad\"))\n",
    "print(sentiment_inference(lstm_model, \"This film will be houseful for weeks\"))\n",
    "print(sentiment_inference(lstm_model, \"I just really loved the movie\"))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.18"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
